
#include "stdafx.h"
#include "generator_cache.h"

using namespace std;

const char * TAG_INPUT_BEGIN     = "// @auto_in: ";
const char * TAG_INPUT_BEGIN_2   = "/* @auto_in: ";
const char * TAG_OUTPUT_BEGIN    = "// @auto_out: ";
const char * TAG_INPUT_END       = "// @auto_in_end";
const char * TAG_INPUT_END_2     = "*/";
const char * TAG_OUTPUT_END      = "// @auto_out_end";

void GENERATOR_CACHE::ResetExtensions(void)
{
   extensions.Clear();
}

GENERATOR_CACHE::~GENERATOR_CACHE(void)
{
   for (int i = 0; i < gens.Length(); ++i) {
      delete gens[i].callback;
   }
   gens.Clear();
}

/******************************************************************************/
/* GENERATOR_CACHE::AddExtension
/*
/******************************************************************************/
void GENERATOR_CACHE::AddExtension(const char * ext)
{
   extensions += ext;
}

/******************************************************************************/
/* GENERATOR_CACHE::AddBlock
/*
/******************************************************************************/
void GENERATOR_CACHE::AddGenerator(const dsSTRING & name, GENERATOR_CALLBACK * callback)
{
   GENERATOR_INFO_T info;
   info.name = name;
   info.callback = callback;

   gens += info;
}

/******************************************************************************/
/* GENERATOR_CACHE::NormalizePath
/*
/******************************************************************************/
dsSTRING GENERATOR_CACHE::NormalizePath(dsSTRING path)
{
   path.ToLower();
   path = fnmNormalizePath(path);

   if (!fnmIsDelim(path[path.Length() - 1])) {
      path += FNM_SLASH;
   }

   return path;
}

/******************************************************************************/
/* GENERATOR_CACHE::Search
/*
/******************************************************************************/
bool GENERATOR_CACHE::Search(const char * path, const dsVECTOR<dsSTRING> & pathsToExclude)
{
   dsSTRING searchPath = NormalizePath(path);

   dsVECTOR<dsSTRING> exclude;
   for (int i = 0; i < pathsToExclude.Length(); ++i) {
      exclude += NormalizePath(pathsToExclude[i]);
   }
   ds::Sort(exclude);

   DoSearch(searchPath, exclude);

   return errorsCount == 0;
}

/******************************************************************************/
/* GENERATOR_CACHE::DoSearch
/*
/******************************************************************************/
void GENERATOR_CACHE::DoSearch(const dsSTRING & path, const dsVECTOR<dsSTRING> & exclude)
{
   dsSTRING query = path + "*.*";

   if (exclude.FindSorted(path) != -1) {
      return ;
   }

   WIN32_FIND_DATA findData;
   HANDLE find = FindFirstFile(query.CStr(), &findData);
   if (find == NULL || find == INVALID_HANDLE_VALUE) {
      return ;
   }

   do {
      if (findData.cFileName[0] == '.') {
         continue;
      }

      if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
         DoSearch(NormalizePath(path + findData.cFileName + FNM_SLASH), exclude);

         continue;
      }

      dsSTRING ext = fnmGetExtStr(findData.cFileName);
      bool process = false;
      for (int i = 0; i < extensions.Length(); ++i) {
         if (ext == extensions[i]) {
            process = true;
            break;
         }
      }

      if (!process) {
         continue;
      }

      dsSTRING filename = path + findData.cFileName;
      filename.ToLower();

      FILE_INFO_T & info = cache[filename];

      osGetFileTime(filename.CStr(), info.newTime);

      if (info.time != info.newTime) {
         ++newFiles;
         DoProcessFile(filename, info);
      }
   } while (FindNextFile(find, &findData));

   return ;
}

/******************************************************************************/
/* GENERATOR_CACHE::DoProcessFile
/*
/******************************************************************************/
void GENERATOR_CACHE::DoProcessFile(const dsSTRING & filename, FILE_INFO_T & info)
{
   ifstream input(filename.CStr());
   
   dsVECTOR<BLOCK_INFO_T> blocks;

   int lineCount = 0;
   string line;
   while (getline(input, line)) {
      ++lineCount;

      const char * cur = line.c_str();
      while (*cur && ds::IsSpace(*cur)) {
         ++cur;
      }

      if (*cur == 0 || *cur != '/') {
         continue;
      }

      BLOCK_INFO_T block;
      block.lineBegin = lineCount - 1;
      block.isInput = false;

      int  isInputTag = 0;
      bool isOutputTag = 0;

      if (equals_n(cur, TAG_INPUT_BEGIN)) { // check tag: // @auto_in:
         isInputTag = 1;
         cur += strlen(TAG_INPUT_BEGIN);
      } else if (equals_n(cur, TAG_INPUT_BEGIN_2)) { // check tag: /* @auto_in:
         isInputTag = 2;
         cur += strlen(TAG_INPUT_BEGIN_2);
      } else if (equals_n(cur, TAG_OUTPUT_BEGIN)) { // check tag: // @auto_out:
         isOutputTag = true;
         cur += strlen(TAG_OUTPUT_BEGIN);
      }

      // this is not a multigen tag, skip it
      if (!isInputTag && !isOutputTag) {
         continue;
      }

      if (isInputTag) {
         block.isInput = true;
      }

      while (*cur && ds::IsSpace(*cur)) {
         ++cur;
      }

      if (*cur == 0) {
         AddErrorMsg(filename, lineCount, "invalid tag");
         return ;
      }

      dsSTRING generatorName;
      while (*cur != 0 && *cur != ':') {
         generatorName += *cur;
         ++cur;
      }
      generatorName.Trim();

      // search generator with such name
      for (int k = 0; k < gens.Length(); ++k) {
         if (gens[k].name == generatorName) {
            block.generatorName = gens[k].name;
            block.generator = gens[k].callback;
            break;
         }
      }

      if (block.generator == NULL) {
         AddErrorMsg(filename, lineCount, "'" + generatorName + "': invalid generator name");
         return ;
      }

      // params
      if (*cur == ':') {
         do {
            ++cur;
         } while (*cur != 0 && ds::IsSpace(*cur));
      }

      block.params = cur;

      // search for block end
      while (getline(input, line)) {
         ++lineCount;

         dsSTRING tag = dsSTRING(line.c_str()).ToTrimmed();

         if (isInputTag == 1 && tag == TAG_INPUT_END
         ||  isInputTag == 2 && tag == TAG_INPUT_END_2
         ||  isOutputTag     && tag == TAG_OUTPUT_END) {
            block.lineEnd = lineCount - 1;
            break;
         }
      }

      if (block.lineEnd == -1) {
         AddErrorMsg(filename, block.lineBegin + 1, "'" + block.generatorName + "': block end not found");
         return ;
      }

      blocks += block;
   }

   info.blocks = blocks;
   info.time = info.newTime; // successfully parsed, save last time
}

/******************************************************************************/
/* GENERATOR_CACHE::ResetErrors
/*
/******************************************************************************/
void GENERATOR_CACHE::ResetErrors(void)
{
   errorsCount = 0;
   errorMsg.Clear();
}

/******************************************************************************/
/* GENERATOR_CACHE::AddErrorMsg
/*
/******************************************************************************/
void GENERATOR_CACHE::AddErrorMsg(const dsSTRING & file, int line, const dsSTRING & msg)
{
   errorMsg += _PR("%s(%d): error: %s\n", file.CStr(), line, msg.CStr());
   ++errorsCount;
}

/******************************************************************************/
/* GENERATOR_CACHE::ReadLines
/*
/******************************************************************************/
bool GENERATOR_CACHE::ReadLines(const dsSTRING & filename, dsVECTOR<dsSTRING> & lines)
{
   ifstream input(filename.CStr());

   if (!input) {
      AddErrorMsg(filename, -1, "failed to open file for reading");
      return false;
   }

   string line;
   while (getline(input, line)) {
      lines += line.c_str();
   }

   return true;
}

/******************************************************************************/
/* GENERATOR_CACHE::Execute
/*
/******************************************************************************/
bool GENERATOR_CACHE::Execute(void)
{
   GENERATOR_ERROR_INFO errorInfo;

   for (int k = 0; k < gens.Length(); ++k) {
      if (!gens[k].callback->Begin(errorInfo)) {
         errorMsg += errorInfo.errorMsg;
         errorsCount += errorsCount;

         return false;
      }
   }

   std::map<dsSTRING, dsVECTOR<dsSTRING> > linesMap;

   // input data
   for (FILE_INFO_MAP_T::const_iterator iter = cache.begin(); iter != cache.end(); ++iter) {
      if (iter->second.blocks.IsEmpty()) {
         continue;
      }

      dsVECTOR<dsSTRING> & lines = linesMap[iter->first];
      if (!ReadLines(iter->first, lines)) {
         continue;
      }

      for (int k = 0; k < iter->second.blocks.Length(); ++k) {
         const BLOCK_INFO_T & block = iter->second.blocks[k];

         if (!block.isInput) {
            continue;
         }

         dsSTRING inputText;
         for (int kkk = block.lineBegin + 1; kkk < block.lineEnd; ++kkk) {
            inputText += lines[kkk];
            inputText += "\n";
         }

         errorInfo.Init(block.generatorName, iter->first, block.lineBegin + 1);
         if (!block.generator->Input(block.params, inputText, errorInfo)) {
            STRONG_ASSERT(errorInfo.errorsCount > 0);
            errorMsg += errorInfo.errorMsg;
            errorsCount += errorInfo.errorsCount;
            return false;
         }
      }
   }

   // output data
   for (FILE_INFO_MAP_T::const_iterator iter = cache.begin(); iter != cache.end(); ++iter) {
      if (iter->second.blocks.IsEmpty()) {
         continue;
      }
      
      dsVECTOR<dsSTRING> & lines = linesMap[iter->first];

      bool updateFile = false;
      for (int k = 0; k < iter->second.blocks.Length(); ++k) {
         const BLOCK_INFO_T & block = iter->second.blocks[k];

         if (block.isInput) {
            continue;
         }

         dsSTRING oldOutputText;
         for (int kkk = block.lineBegin + 1; kkk < block.lineEnd; ++kkk) {
            oldOutputText += lines[kkk];
            if (kkk + 1 < block.lineEnd) {
               oldOutputText += "\n";
            }
         }

         errorInfo.Init(block.generatorName, iter->first, block.lineBegin + 1);
         if (!block.generator->Output(block.params, block.outputText, errorInfo)) {
            STRONG_ASSERT(errorInfo.errorsCount > 0);
            errorMsg += errorInfo.errorMsg;
            errorsCount += errorsCount;
            return false;
         }

         if (block.outputText != oldOutputText) {
            updateFile = true;
         }
      }

      if (updateFile) {
         INT64 time;
         if (!osGetFileTime(iter->first.CStr(), time) || time != iter->second.newTime) {
            printf("Warning: file '%s' has been changed, skipping generation\n", iter->first.CStr());
            continue; // file has been modified, don't touch it
         }

         ++updatedFiles;

         ofstream output(iter->first.CStr());

         int index = 0;
         while (iter->second.blocks[index].isInput) {
            ++index;
         }

         const BLOCK_INFO_T * cur = &iter->second.blocks[index];
         for (int k = 0; k < lines.Length(); ++k) {
            if (cur == NULL || k <= cur->lineBegin) {
               output << lines[k] << endl;
               continue;
            }

            output << cur->outputText << endl;

            k = cur->lineEnd;
            output << lines[cur->lineEnd] << endl;

            do {
               ++index;
            } while (index < iter->second.blocks.Length() && iter->second.blocks[index].isInput);
            
            if (index >= iter->second.blocks.Length()) {
               cur = NULL;
            } else {
               cur = &iter->second.blocks[index];
            }
         }
      }

      for (int k = 0; k < iter->second.blocks.Length(); ++k) {
         iter->second.blocks[k].outputText.Clear();
      }
   }

   for (int k = 0; k < gens.Length(); ++k) {
      errorInfo.Init("", "", -1);
      if (!gens[k].callback->End(errorInfo)) {
         errorMsg += errorInfo.errorMsg;
         errorsCount += errorsCount;
         return false;
      }
   }

   return errorsCount == 0;
}

namespace fio {
   void Serialize(fioFILE & file, const GENERATOR_CACHE::BLOCK_INFO_T & p, void * info = NULL)
   {
      fio::SERIALIZER(file, info)
         << p.generatorName
         << p.isInput
         << p.params
         << p.lineBegin
         << p.lineEnd;
   }

   void Deserialize(fioFILE & file, GENERATOR_CACHE::BLOCK_INFO_T & p, void * info)
   {
      fio::DESERIALIZER(file, info)
         >> p.generatorName
         >> p.isInput
         >> p.params
         >> p.lineBegin
         >> p.lineEnd;

      GENERATOR_CACHE * cache = (GENERATOR_CACHE *)info;
      for (int i = 0; i < cache->gens.Length(); ++i) {
         if (p.generatorName == cache->gens[i].name) {
            p.generator = cache->gens[i].callback;
            return ;
         }
      }

      throw fio::SERIALIZATION_ERROR("Unknown generator name '" + p.generatorName + "'");
   }

   void Serialize(fioFILE & file, const GENERATOR_CACHE::FILE_INFO_T & p, void * info = NULL)
   {
      fio::SERIALIZER(file, info)
         << p.time
         << p.blocks;
   }

   void Deserialize(fioFILE & file, GENERATOR_CACHE::FILE_INFO_T & p, void * info)
   {
      fio::DESERIALIZER(file, info)
         >> p.time
         >> p.blocks;
   }
}

/******************************************************************************/
/* GENERATOR_CACHE::Save
/*
/******************************************************************************/
void GENERATOR_CACHE::Save(const char * filename)
{
   fioFILE_DISK file;
   if (!file.InitFile(filename, FIO_OPEN_W)) {
      return ;
   }

   fio::SERIALIZER serializer(file);

   for (FILE_INFO_MAP_T::const_iterator iter = cache.begin(); iter != cache.end(); ++iter) {
      serializer << iter->first << iter->second;
   }

   file.Term();
}

/******************************************************************************/
/* GENERATOR_CACHE::Load
/*
/******************************************************************************/
void GENERATOR_CACHE::Load(const char * filename)
{
   try {
      fioFILE_DISK file;
      if (!file.InitFile(filename, FIO_OPEN_R)) {
         return ;
      }

      while (!file.IsEOF()) {
         dsSTRING filename;

         fio::DESERIALIZER(file, this) >> filename;
         fio::DESERIALIZER(file, this) >> cache[filename];
      }

      file.Term();
   }
   catch (const fio::SERIALIZATION_ERROR & e) {
      osOutputLine("Serialization error: " << e.message);
   }
}

/******************************************************************************/
/* genParseArgs
/*
/******************************************************************************/
void genParseArgs(const dsSTRING & params_, dsVECTOR<dsSTRING> & res)
{
   const dsSTRING params = params_ + " ";

   enum {
      DEFAULT,
      STRING_1,   // '
      STRING_2,   // "
   } state = DEFAULT;

   dsSTRING val;
   for (int index = 0; index < params.Length(); ++index) {
      char cur = params[index];

      switch (state) {
         case DEFAULT:
            if (ds::IsSpace(cur) || cur == '"' || cur == '\'') {
               if (!val.IsEmpty()) {
                  res += val;
                  val.Clear();
               }

               if (cur == '\'') {
                  state = STRING_1;
               }
               if (cur == '"') {
                  state = STRING_2;
               }
               continue;
            }

            val += cur;
            break;

         case STRING_1:
         case STRING_2:
            if (cur == '\\') {
               val += params[index + 1];
               ++index;
               continue;
            }
            if (state == STRING_1 && cur == '\'' || state == STRING_2 && cur == '"') {
               res += val;
               val.Clear();
               state = DEFAULT;
               continue;
            }

            val += cur;
            break;
      }
   }

   if (!val.IsEmpty()) {
      res += val;
   }
}